package org.shj.spring.boot.base;

import com.google.common.base.Splitter;
import com.google.common.collect.Lists;

import com.querydsl.jpa.impl.JPAQueryFactory;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.shj.spring.boot.base.orm.SqlTerm;
import org.shj.spring.boot.base.param.*;
import org.shj.spring.boot.lang.BeanMapper;
import org.shj.spring.boot.lang.Validate;
import org.shj.spring.boot.lang.exception.NotSupportedException;
import org.shj.spring.boot.util.StringUtil;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.QueryDslJpaRepository;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.*;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import static java.util.stream.Collectors.toList;
import static org.shj.spring.boot.util.JpaUtil.*;

@NoRepositoryBean
public class BaseDaoImpl<E extends BaseEntity<PK>, PK extends Serializable> extends QueryDslJpaRepository<E, PK> implements BaseDao<E, PK>{

    private final Class<E> entityClass;

    private final EntityManager entityManager;

    private JPAQueryFactory queryFactory;

    public BaseDaoImpl(JpaEntityInformation<E, PK> entityInformation, EntityManager entityManager) {
        super(entityInformation, entityManager);
        this.entityClass = entityInformation.getJavaType();
        this.queryFactory = new JPAQueryFactory(entityManager);
        this.entityManager = entityManager;
    }

    public PK insert(E e) {
        Validate.notNull(e);
        E entity = save(e);
        return entity.getId();
    }

    @Transactional(rollbackFor = RuntimeException.class)
    public int update(UpdateParam<E> updateParam) {
        E entity = updateParam.getData();
        List<E> origins = query(updateParam);
        updateParam.getExcludes().add(BaseEntity.ID);
        if (CollectionUtils.isNotEmpty(origins)) {
            for (E e : origins) {
                org.shj.spring.boot.lang.BeanMapper.patch(entity, e, updateParam.getIncludes(), updateParam.getExcludes());
                save(e);
            }
        }
        return CollectionUtils.size(origins);
    }

    @Transactional(rollbackFor = RuntimeException.class)
    public <Q extends Param> int delete(Q entity) {
        List<E> entities = query(entity);
        if (CollectionUtils.isNotEmpty(entities)) {
            delete(entities);
        }
        return CollectionUtils.size(entities);
    }

    public int deleteByPk(PK pk) {
        Validate.notNull(pk);
        E entity = findOne(pk);
        if (entity == null) {
            return 0;
        }
        delete(entity);
        return 1;
    }

    public <Q extends Param> List<E> query(Q queryEntity) {
        Specification specification = buildSpecification(queryEntity);
        if (isPaging(queryEntity)) {
            Pageable pageable = buildPageRequest((QueryParam) queryEntity);
            Page<E> result = findAll(specification, pageable);
            return result.getContent();
        } else {
            return findAll(specification);
        }
    }

    public <Q extends Param> long count(Q queryEntity) {
        Specification specification = buildSpecification(queryEntity);
        return count(specification);
    }

    protected Specification buildSpecification(Param param) {
        Specification specification = (root, query, cb) -> {
            query.distinct(checkIfNeedUnique(param));
            List<Predicate> predicateList = Lists.newArrayList();
            predicateList.addAll(buildSpecificationByPropertyFilter(param.getTerms(), root, cb));
            return cb.and(predicateList.toArray(new Predicate[predicateList.size()]));
        };
        return specification;
    }

    private boolean checkIfNeedUnique(Param param) {
        return param.isDistinct() && param.getTerms().stream().anyMatch(term -> StringUtils.contains(term.getColumn(), "."));
    }

    protected List<Predicate> buildSpecificationByPropertyFilter(List<Term> terms, Root root, CriteriaBuilder cb) {
        List<Predicate> predicateList = Lists.newArrayList();
        for (Term term : terms) {
            Predicate predicate = buildPredicate(term, root, cb);
            if (predicate != null) {
                predicateList.add(predicate);
            }
        }
        return predicateList;
    }

    protected Predicate buildPredicate(Term term, Root root, CriteriaBuilder cb) {
        if (term instanceof SqlTerm) {
            return null;
        }
        boolean nullTerm = StringUtil.isNullOrEmpty(term.getColumn());
        //是空条件，但是无嵌套
        if (nullTerm && term.getTerms().isEmpty()) {
            return null;
        }
        Predicate predicate = null;
        if (!nullTerm) {
            predicate = buildPredicate(getPropertyPath(root, term.getColumn()), term.getValue(), term.getTermType(), cb);
        }
        if (CollectionUtils.isNotEmpty(term.getTerms())) {
            for (Term innerTerm : term.getTerms()) {
                Predicate tmp = buildPredicate(innerTerm, root, cb);
                switch (innerTerm.getType()) {
                    case and:
                        predicate = cb.and(predicate, tmp);
                        break;
                    case or:
                        predicate = cb.or(predicate, tmp);
                        break;
                    default:
                }
            }
        }
        return predicate;
    }

    protected Path getPropertyPath(Root root, String propertyName) {
        if (StringUtils.contains(propertyName, ".")) {
            List<String> propertyNames = Splitter.on(".").splitToList(propertyName);

            Join join = null;
            for (int i = 0; i < propertyNames.size() - 1; i++) {
                String joinedPropertyName = propertyNames.get(i);
                if (join == null) {
                    join = root.join(joinedPropertyName);
                } else {
                    join = join.join(joinedPropertyName);
                }
            }
            return join.get(propertyNames.get(propertyNames.size() - 1));

        } else {
            return root.get(propertyName);
        }
    }

    /**
     * 按属性条件参数创建Predicate,辅助函数.
     */
    protected Predicate buildPredicate(final Path propertyPath,
                                       final Object propertyValue, final TermEnum termType, CriteriaBuilder cb) {
        Validate.notNull(propertyPath, "propertyPath can't be null");
        Object value = getObject(propertyPath, propertyValue, termType);
        // 根据TermType构造criterion
        Predicate predicate;
        switch (termType) {
            case eq:
                predicate = cb.equal(propertyPath, value);
                break;
            case like:
                String likeValue = StringUtil.toString(value);
                predicate = cb.like(propertyPath, StringUtils.contains(likeValue, "%") ? likeValue : "%" + likeValue + "%");
                break;
            case lte:
                predicate = cb.le(propertyPath, (Number) value);
                break;
            case lt:
                predicate = cb.lt(propertyPath, (Number) value);
                break;
            case gte:
                predicate = cb.ge(propertyPath, (Number) value);
                break;
            case gt:
                predicate = cb.gt(propertyPath, (Number) value);
                break;
            case notnull:
                predicate = cb.isNotNull(propertyPath);
                break;
            case isnull:
                predicate = cb.isNull(propertyPath);
                break;
            case not:
                predicate = cb.notEqual(propertyPath, value);
                break;
            case nlike:
                predicate = cb.notLike(propertyPath, StringUtil.toString(value));
                break;
            case in:
                predicate = propertyPath.in((List) value);
                break;
            case nin:
                predicate = cb.not(propertyPath.in((List) value));
                break;
            case btw:
                Validate.isTrue(CollectionUtils.size(value) == 2, "between must have 2 params using ',' splitter.");
                predicate = cb.between(propertyPath, (Comparable) ((List) value).get(0), (Comparable) ((List) value).get(1));
                break;
            case nbtw:
                Validate.isTrue(CollectionUtils.size(value) == 2, "between must have 2 params using ',' splitter.");
                predicate = cb.not(cb.between(propertyPath, (Comparable) ((List) value).get(0), (Comparable) ((List) value).get(1)));
                break;
            case empty:
                predicate = cb.isEmpty(propertyPath);
                break;
            case nempty:
                predicate = cb.isNotEmpty(propertyPath);
                break;
            default:
                throw new NotSupportedException(String.format("operator[%s] not supported." + termType));

        }
        return predicate;
    }

    protected Object getObject(Path propertyPath, Object propertyValue, TermEnum matchType) {
        Object value = null;
        if (propertyValue != null) {
            switch (matchType) {
                case in:
                case nin:
                case btw:
                case nbtw:
                    if (propertyValue instanceof Collection) {
                        value = ((Collection) propertyValue).stream().map(obj -> BeanMapper.deepMap(obj, propertyPath.getJavaType())).collect(toList());
                    } else {
                        List<String> valueStrs = Splitter.on(",").omitEmptyStrings().splitToList(StringUtil.toString(propertyValue));
                        List<Object> valueList = Lists.newArrayList();
                        for (String valueStr : valueStrs) {
                            valueList.add(BeanMapper.deepMap(valueStr, propertyPath.getJavaType()));
                        }
                        value = valueList;
                    }
                    break;
                case notnull:
                case isnull:
                    break;
                default:
                    value = BeanMapper.deepMap(propertyValue, propertyPath.getJavaType());
            }
        }
        return value;
    }

    @Override
    @Transactional(readOnly = true)
    public List<E> selectByNativeSql(String sql) {
        Query query = entityManager.createNativeQuery(sql, getEntityClass());
        return query.getResultList();
    }

    @Override
    @Transactional(readOnly = true)
    public List<E> selectByHql(String hql, Map<String, ?> params) {
        TypedQuery<E> typedQuery = entityManager.createQuery(hql, getEntityClass());
        if (MapUtils.isNotEmpty(params)) {
            for (Map.Entry<String, ?> entry : params.entrySet()) {
                typedQuery.setParameter(entry.getKey(), entry.getValue());
            }
        }
        return typedQuery.getResultList();
    }

    @Override
    public JPAQueryFactory queryFactory() {
        return queryFactory;
    }


    public Class<E> getEntityClass() {
        return entityClass;
    }

    public EntityManager getEntityManager() {
        return entityManager;
    }

    public JPAQueryFactory getQueryFactory() {
        return queryFactory;
    }

    public void setQueryFactory(JPAQueryFactory queryFactory) {
        this.queryFactory = queryFactory;
    }
}
